﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Globalization;
using System.Threading;

namespace System.Windows.Elements
{
    public class NumericBox : TextBox
    {
        private const String Minus = "-";
        private Object syncLock = new Object();
        private Boolean undoTextChangesFlag, skipValidationFlag, isParsingFlag, coercedWhileParsingFlag;
        private String lastValidText = String.Empty;

        static NumericBox()
        {
            //EventManager.RegisterClassHandler(
            //    typeof(NumericTextBox),
            //    DataObject.PastingEvent,
            //    (DataObjectPastingEventHandler)((sender, e) =>
            //    {
            //        if (!IsDataValid(e.DataObject))
            //        {
            //            DataObject data = new DataObject();
            //            data.SetText(String.Empty);
            //            e.DataObject = data;
            //            e.Handled = false;
            //        }
            //    }));

            //TextBox.TextProperty.OverrideMetadata(
            //    typeof(NumericTextBox),
            //    new FrameworkPropertyMetadata(
            //        String.Empty,
            //        FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            //        null,
            //        (DependencyObject element, Object baseValue) =>
            //        {
            //            return this.TryParse(baseValue.ToString()) ? baseValue : null;
            //        }));
        }

        //protected override void OnDrop(DragEventArgs e)
        //{
        //    e.Handled = !this.TryParse(e.Data);
        //    base.OnDrop(e);
        //}

        //protected override void OnDragOver(DragEventArgs e)
        //{
        //    if (!this.TryParse(e.Data))
        //    {
        //        e.Handled = true;
        //        e.Effects = DragDropEffects.None;
        //    }

        //    base.OnDragEnter(e);
        //}

        private Boolean TryParse(IDataObject data)
        {
            Boolean isValid = false;
            if (data != null)
            {
                String text = data.GetData(DataFormats.Text) as String;
                isValid = this.TryParse(text);
            }
            return isValid;
        }

        private Boolean TryParse(String text)
        {
            this.isParsingFlag = true;

            Boolean isValid = false;
            Decimal? value = null;
            if (String.IsNullOrEmpty(text))
            {
                //isValid = this.IsNullAllowed;
                isValid = true;
                if (!this.IsNullAllowed) value = 0;
            }
            else if (text.Trim() == NumericBox.Minus)
            {
                isValid = true;
                if (!this.IsNullAllowed) value = 0;
            }
            else if (this.IsIntegerMode)
            {
                Int64 newValue;
                isValid = Int64.TryParse(text, NumberStyles.Integer, null, out newValue);
                if (isValid) value = newValue;
            }
            else
            {
                Decimal newValue;
                isValid = Decimal.TryParse(text, NumberStyles.Number, null, out newValue);
                if (isValid) value = newValue;
            }

            if (isValid) this.Value = value;
            else
            {
                InvalidEntryEventArgs args = new InvalidEntryEventArgs(InvalidEntryEvent, text);
                this.OnInvalidEntry(args);
            }

            this.isParsingFlag = false;
            return isValid;
        }

        protected override void OnPreviewTextInput(TextCompositionEventArgs e)
        {
            Int32 index = this.CaretIndex;
            Int32 selectionStart = this.SelectionStart;
            Int32 selectionLength = this.SelectionLength;
            String text = this.Text;

            if (selectionLength > 0)
            {
                if (index > selectionStart) index -= selectionLength;
                text = text.Remove(selectionStart, selectionLength);
            }

            String newText = (index < text.Length) ? String.Concat(text.Substring(0, index), e.Text, text.Substring(index)) : String.Concat(text, e.Text);

            e.Handled = !this.TryParse(newText);
            if (!e.Handled)
            {
                Monitor.Enter(this.syncLock);
                this.skipValidationFlag = true;
                base.OnPreviewTextInput(e);
            }
        }

        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            if (this.undoTextChangesFlag)
            {
                this.undoTextChangesFlag = false;
                Monitor.Exit(this.syncLock);
            }
            else
            {
                Boolean isValid;
                if (!this.skipValidationFlag) isValid = this.TryParse(this.Text);
                else
                {
                    this.skipValidationFlag = false;
                    Monitor.Exit(this.syncLock);
                    isValid = true;
                }

                if (!isValid)
                {
                    Monitor.Enter(this.syncLock);
                    this.undoTextChangesFlag = true;
                    this.Text = this.lastValidText;
                }
                else
                {
                    if (this.coercedWhileParsingFlag)
                    {
                        this.coercedWhileParsingFlag = false;
                        Monitor.Enter(this.syncLock);
                        this.undoTextChangesFlag = true;
                        this.Text = this.Value.ToString();
                    }
                    this.lastValidText = this.Text;
                    base.OnTextChanged(e);
                }
            }
        }

        #region Value

        /// <summary>
        /// The DependencyProperty for the Value property
        /// </summary>
        public static readonly DependencyProperty ValueProperty =
                DependencyProperty.Register(
                        "Value",
                        typeof(Decimal?),
                        typeof(NumericBox),
                        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(NumericBox.OnValueChanged), new CoerceValueCallback(NumericBox.CoerceValue)));


        /// <summary>
        /// Gets or sets the value of <see cref="NumericTextBox"/>.
        /// </summary>
        public Decimal? Value
        {
            get { return (Decimal?)this.GetValue(NumericBox.ValueProperty); }
            set { this.SetValue(NumericBox.ValueProperty, NumericBox.CoerceValue(this, value)); }
        }

        private static Object CoerceValue(DependencyObject d, Object value)
        {
            NumericBox sender = (NumericBox)d;
            if (value == null)
            {
                if (sender.IsNullAllowed) return null;
                else
                {
                    if (sender.isParsingFlag) sender.coercedWhileParsingFlag = true;
                    return sender.MinValue;
                }
            }
            else
            {
                Decimal newValue = (Decimal)value;

                Decimal min = sender.MinValue;
                if (newValue < min)
                {
                    if (sender.isParsingFlag) sender.coercedWhileParsingFlag = true;
                    return min;
                }

                Decimal max = sender.MaxValue;
                if (newValue > max)
                {
                    if (sender.isParsingFlag) sender.coercedWhileParsingFlag = true;
                    return max;
                }

                if (sender.IsIntegerMode)
                {
                    Decimal newRoundedValue = Math.Round(newValue);
                    if (newValue != newRoundedValue)
                    {
                        if (sender.isParsingFlag) sender.coercedWhileParsingFlag = true;
                        return newRoundedValue;
                    }
                }
                
                return value;
            }
        }

        protected static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            NumericBox sender = (NumericBox)d;
            Decimal? oldValue = (Decimal?)e.OldValue;
            Decimal? newValue = (Decimal?)e.NewValue;

            if (!sender.isParsingFlag)
            {
                Monitor.Enter(sender.syncLock);
                sender.skipValidationFlag = true;
                sender.Text = newValue.ToString();
            }

            RoutedPropertyChangedEventArgs<Decimal?> routedArgs =
                    new RoutedPropertyChangedEventArgs<Decimal?>(oldValue, newValue, NumericBox.ValueChangedEvent);

            sender.OnValueChanged(routedArgs);
        }

        #endregion

        #region IsNullAllowed

        /// <summary>
        /// The DependencyProperty for the IsNullAllowed property
        /// </summary>
        public static readonly DependencyProperty IsNullAllowedProperty =
                DependencyProperty.Register(
                        "IsNullAllowed",
                        typeof(Boolean),
                        typeof(NumericBox),
                        new FrameworkPropertyMetadata(true, new PropertyChangedCallback(NumericBox.OnIsNullAllowedChanged)));


        /// <summary>
        /// Gets or sets if the null value of <see cref="NumericTextBox"/> is allowed.
        /// </summary>
        public Boolean IsNullAllowed
        {
            get { return (Boolean)this.GetValue(NumericBox.IsNullAllowedProperty); }
            set { this.SetValue(NumericBox.IsNullAllowedProperty, value); }
        }

        protected static void OnIsNullAllowedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            NumericBox sender = (NumericBox)d;
            Boolean oldValue = (Boolean)e.OldValue;
            Boolean newValue = (Boolean)e.NewValue;

            if (!newValue && !sender.Value.HasValue) sender.Value = sender.MinValue;
        }

        #endregion

        #region IsIntegerMode

        /// <summary>
        /// The DependencyProperty for the IsIntegerMode property
        /// </summary>
        public static readonly DependencyProperty IsIntegerModeProperty =
                DependencyProperty.Register(
                        "IsIntegerMode",
                        typeof(Boolean),
                        typeof(NumericBox),
                        new FrameworkPropertyMetadata(false, new PropertyChangedCallback(NumericBox.OnIsIntegerModeChanged)));


        /// <summary>
        /// Gets or sets if the value of <see cref="NumericTextBox"/> is integer or decimal.
        /// </summary>
        public Boolean IsIntegerMode
        {
            get { return (Boolean)this.GetValue(NumericBox.IsIntegerModeProperty); }
            set { this.SetValue(NumericBox.IsIntegerModeProperty, value); }
        }

        protected static void OnIsIntegerModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            NumericBox sender = (NumericBox)d;
            Boolean oldValue = (Boolean)e.OldValue;
            Boolean newValue = (Boolean)e.NewValue;

            if (newValue)
            {
                sender.CoerceValue(NumericBox.MinValueProperty);
                sender.CoerceValue(NumericBox.MaxValueProperty);
                sender.CoerceValue(NumericBox.ValueProperty);
            }
        }

        #endregion

        #region Max/MinValue

        /// <summary>
        /// The DependencyProperty for the MinValue property.
        /// </summary>
        public static readonly DependencyProperty MinValueProperty =
            DependencyProperty.Register(
                        "MinValue",
                        typeof(Decimal),
                        typeof(NumericBox),
                        new FrameworkPropertyMetadata(Decimal.Zero, new PropertyChangedCallback(NumericBox.OnMinValueChanged), new CoerceValueCallback(NumericBox.CoerceMinValue)));

        /// <summary>
        /// The min value of <see cref="NumericTextBox"/>.
        /// </summary>
        public Decimal MinValue
        {
            get { return (Decimal)this.GetValue(NumericBox.MinValueProperty); }
            set { this.SetValue(NumericBox.MinValueProperty, value); }
        }

        private static void OnMinValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            NumericBox sender = (NumericBox)d;

            sender.CoerceValue(NumericBox.MaxValueProperty);
            sender.CoerceValue(NumericBox.ValueProperty);
        }

        private static Object CoerceMinValue(DependencyObject d, Object value)
        {
            NumericBox sender = (NumericBox)d;
            if (sender.IsIntegerMode) return Math.Round((Decimal)value);
            else return value;
        }

        /// <summary>
        /// The DependencyProperty for the MaxValue property.
        /// </summary>
        public static readonly DependencyProperty MaxValueProperty =
            DependencyProperty.Register(
                        "MaxValue",
                        typeof(Decimal),
                        typeof(NumericBox),
                        new FrameworkPropertyMetadata(Decimal.MaxValue, new PropertyChangedCallback(NumericBox.OnMaxValueChanged), new CoerceValueCallback(NumericBox.CoerceMaxValue)));

        /// <summary>
        /// The max value of <see cref="NumericTextBox"/>.
        /// </summary>
        public Decimal MaxValue
        {
            get { return (Decimal)this.GetValue(NumericBox.MaxValueProperty); }
            set { this.SetValue(NumericBox.MaxValueProperty, value); }
        }

        private static void OnMaxValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            NumericBox sender = (NumericBox)d;

            sender.CoerceValue(NumericBox.ValueProperty);
        }

        private static Object CoerceMaxValue(DependencyObject d, Object value)
        {
            NumericBox sender = (NumericBox)d;
            Decimal newValue = (Decimal)value;

            Decimal min = sender.MinValue;
            if (newValue < min) return min;
            else if (sender.IsIntegerMode) return Math.Round((Decimal)value);
            else return value;
        }

        #endregion

        #region Public Events

        /// <summary>
        /// InvalidEntry event
        /// </summary>
        public static readonly RoutedEvent InvalidEntryEvent =
                    EventManager.RegisterRoutedEvent("InvalidEntry", RoutingStrategy.Bubble, typeof(InvalidEntryEventHandler), typeof(NumericBox));

        /// <summary>
        /// Add / Remove InvalidEntry handler
        /// </summary>
        public event InvalidEntryEventHandler InvalidEntry
        {
            add { this.AddHandler(NumericBox.InvalidEntryEvent, value); }
            remove { this.RemoveHandler(NumericBox.InvalidEntryEvent, value); }
        }

        /// <summary>
        /// Event ID correspond to Value changed event
        /// </summary>
        public static readonly RoutedEvent ValueChangedEvent =
                    EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<Decimal?>), typeof(NumericBox));

        /// <summary>
        /// An event reporting that the Value property changed.
        /// </summary>
        public event RoutedPropertyChangedEventHandler<Decimal?> ValueChanged
        {
            add { this.AddHandler(NumericBox.ValueChangedEvent, value); }
            remove { this.RemoveHandler(NumericBox.ValueChangedEvent, value); }
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// This event is invoked when datepicker can't parse the input string correctly
        /// </summary>
        protected virtual void OnInvalidEntry(InvalidEntryEventArgs e)
        {
            RaiseEvent(e);
        }

        /// <summary>
        /// This method is invoked when the Value property changes.
        /// </summary>
        /// <param name="e">RoutedPropertyChangedEventArgs contains the old and new value.</param>
        protected virtual void OnValueChanged(RoutedPropertyChangedEventArgs<Decimal?> e)
        {
            RaiseEvent(e);
        }

        #endregion
    }
}
